/**
 * Copyright (c) 2015-present, Facebook, Inc.
 * All rights reserved.
 *
 * This source code is licensed under the license found in the
 * LICENSE file in the root directory of this source tree.
 */

/**
 * @file
 * transform video filter version 1
 */

#include "libavutil/avassert.h"
#include "libavutil/avstring.h"
#include "libavutil/eval.h"
#include "libavutil/imgutils.h"
#include "libavutil/internal.h"
#include "libavutil/opt.h"
#include "libavutil/mem.h"
#include "libavutil/parseutils.h"
#include "avfilter.h"
#include "internal.h"
#include "video.h"


static const char *const var_names[] = {
    "out_w",  "ow",
    "out_h",  "oh",
    NULL
};

enum var_name {
    VAR_OUT_W, VAR_OW,
    VAR_OUT_H, VAR_OH,
    VARS_NB
};

/*
   MMMMT
   MMMMB
   */
#define MAIN_PLANE_WIDTH (8.0f / 9.0f)

#define RIGHT   0
#define LEFT    1
#define TOP     2
#define BOTTOM  3
#define FRONT   4
#define BACK    5

#define UNPACK_ID(pair) ((pair) >> 6)
#define UNPACK_COUNT(pair) ((uint8_t) (pair) & 0x3F)
#define PACK_PAIR(id, count) (((id) << 6) ^ (count))

#define SOFTWARE_PREFETCH_OPT

static const int INITIAL_PAIR_SIZE = 2;

static const float PH = 0.25f;

static const float YC_TOP = 0.75f;
static const float YC_BOTTOM = 0.25f;

static const float Y_HALF = 1.0f / 2.0f;
static const float X_HALF = 1.0f / 2.0f;

// cube transform parameters
static const float P0[] = {-0.5f,-0.5f,-0.5f };
static const float P1[] = { 0.5f,-0.5f,-0.5f };
static const float P4[] = {-0.5f,-0.5f, 0.5f };
static const float P5[] = { 0.5f,-0.5f, 0.5f };
static const float P6[] = {-0.5f, 0.5f, 0.5f };

static const float PX[] = { 1.0f, 0.0f, 0.0f};
static const float PY[] = { 0.0f, 1.0f, 0.0f};
static const float PZ[] = { 0.0f, 0.0f, 1.0f};
static const float NX[] = {-1.0f, 0.0f, 0.0f};
static const float NZ[] = { 0.0f, 0.0f,-1.0f};

static const int PLANE_POLES_FACE_MAP[] = {1, 4, 0, 5, 2, 3};
static const int PLANE_CUBEMAP_FACE_MAP[] = {1, 4, 0, 5, 2, 3};
static const int PLANE_CUBEMAP_32_FACE_MAP[] = {1, 4, 0, 5, 3, 2};

typedef enum StereoFormat {
    STEREO_FORMAT_TB,
    STEREO_FORMAT_LR,
    STEREO_FORMAT_MONO,
    STEREO_FORMAT_GUESS,

    STEREO_FORMAT_N
} StereoFormat;

typedef enum Layout {
    LAYOUT_CUBEMAP,
    LAYOUT_CUBEMAP_32,
    LAYOUT_CUBEMAP_180,
    LAYOUT_PLANE_POLES,
    LAYOUT_PLANE_POLES_6,
    LAYOUT_PLANE_POLES_CUBEMAP,
    LAYOUT_PLANE_CUBEMAP,
    LAYOUT_PLANE_CUBEMAP_32,
    LAYOUT_FLAT_FIXED,
    LAYOUT_BARREL,
    LAYOUT_TB_ONLY,
    LAYOUT_TB_BARREL_ONLY,

    LAYOUT_N
} Layout;

typedef struct TransformPixelWeights {
    uint32_t *pairs;
    uint8_t n;
} TransformPixelWeights;

typedef struct TransformPlaneMap {
    int w, h;
    TransformPixelWeights *weights;
} TransformPlaneMap;

typedef struct TransformContext {
    const AVClass *class;
    int w, h;
    TransformPlaneMap *out_map;
    int out_map_planes;

    AVDictionary *opts;
    char *w_expr;               ///< width  expression string
    char *h_expr;               ///< height expression string
    char *size_str;
    int cube_edge_length;
    int max_cube_edge_length;
    int output_layout;
    int input_stereo_format;
    int vflip;
    int planes;
    int w_subdivisions, h_subdivisions;
    float main_plane_ratio;
    float expand_coef;
    float fixed_yaw;    ///< Yaw (asimuth) angle, degrees
    float fixed_pitch;  ///< Pitch (elevation) angle, degrees
    float fixed_hfov;   ///< Horizontal field of view, degrees
    float fixed_vfov;   ///< Vertical field of view, degrees
} TransformContext;

typedef struct ThreadData {
    TransformPlaneMap *p;
    int subs;
    int linesize;
    int num_tiles;
    int num_tiles_col;
    uint8_t* in_data;
    uint8_t* out_data;
} ThreadData;

static int query_formats(AVFilterContext *ctx)
{
    static const enum AVPixelFormat pix_fmts[] = {
    AV_PIX_FMT_GBRP,
    AV_PIX_FMT_YUV410P,
    AV_PIX_FMT_YUV411P,
    AV_PIX_FMT_YUV420P,
    AV_PIX_FMT_YUV422P,
    AV_PIX_FMT_YUV440P,
    AV_PIX_FMT_YUV444P,
    AV_PIX_FMT_NONE
    };
    AVFilterFormats *fmts_list = ff_make_format_list(pix_fmts);
    if (!fmts_list)
        return AVERROR(ENOMEM);
    return ff_set_common_formats(ctx, fmts_list);
}

// We need to end up with X and Y coordinates in the range [0..1).
// Horizontally wrapping is easy: 1.25 becomes 0.25, -0.25 becomes 0.75.
// Vertically, if we pass through the north pole, we start coming back 'down'
// in the Y direction (ie, a reflection from the boundary) but we also are
// on the opposite side of the sphere so the X value changes by 0.5.
static inline void normalize_equirectangular(float x, float y, float *xout, float *yout) {
    if (y >= 1.0f) {
        // Example: y = 1.25 ; 2.0 - 1.25 = 0.75.
        y = 2.0f - y;
        x += 0.5f;
    } else if (y < 0.0f) {
        y = -y;
        x += 0.5f;
    }

    if (x >= 1.0f) {
        int ipart = (int) x;
        x -= ipart;
    } else if (x < 0.0f) {
        // Example: x = -1.25.  ipart = 1. x += 2 so x = 0.25.
        int ipart = (int) (-x);
        x += (ipart + 1);
    }

    *xout = x;
    *yout = y;
}

static inline void transform_pos(
        TransformContext *ctx,
        float x, float y,
        float *outX, float *outY,
        int *has_mapping,
        float input_pixel_w
        )
{
    int is_right = 0;

    *has_mapping = 1;
    if (ctx->input_stereo_format != STEREO_FORMAT_MONO) {
        if (y > Y_HALF) {
            y = (y - Y_HALF) / Y_HALF;
            if (ctx->vflip) {
                y = 1.0f - y;
            }
            is_right = 1;
        } else {
            y = y / Y_HALF;
        }
    }

    if (ctx->output_layout == LAYOUT_PLANE_POLES) {
        if (x >= ctx->main_plane_ratio) {
            float dx = (x * 2 - 1 - ctx->main_plane_ratio) / (1 - ctx->main_plane_ratio);
            if (y < Y_HALF) {
                // Bottom
                float dy = (y - YC_BOTTOM) / PH;
                *outX = (atan2f(dy, dx)) / (M_PI * 2.0f) + 0.75f;
                *outY = sqrtf(dy * dy + dx * dx) * 0.25f;
            } else {
                // Top
                float dy = (y - YC_TOP) / PH;
                *outX = (atan2f(dy, dx)) / (M_PI * 2.0f) + 0.75f;
                *outY = 1.0f - sqrtf(dy * dy + dx * dx) * 0.25f;
            }
            if (*outX > 1.0f) {
                *outX -= 1.0f;
            }
        } else {
            // Main
            *outX = x / ctx->main_plane_ratio;
            *outY = y * 0.5f + 0.25f;
        }
    } else if (ctx->output_layout == LAYOUT_PLANE_POLES_6) {
        int face = (int) (x * 6);
        if (face < 4) {
            // Main
            *outX = x * 6.0f / 4.0f;
            *outY = y * 0.5f + 0.25f;
        } else {
            float dx, dy;
            x = x * 6.0f - face;
            dx = x * 2 - 1;
            dy = y * 2 - 1;
            if (face == 4) {
                // Top
                *outX = (atan2f(dy, dx)) / (M_PI * 2.0f) + 0.75f;
                *outY = 1.0f - sqrtf(dy * dy + dx * dx) * 0.25f;
            } else {
                // Bottom
                *outX = (atan2f(dy, dx)) / (M_PI * 2.0f) + 0.75f;
                *outY = sqrtf(dy * dy + dx * dx) * 0.25f;
            }
            if (*outX > 1.0f) {
                *outX -= 1.0f;
            }
        }
    } else if (ctx->output_layout == LAYOUT_FLAT_FIXED) {
        // Per the Metadata RFC for orienting the equirectangular coords:
        //                           Heading
        //         -180           0           180
        //       90 +-------------+-------------+   0.0
        //          |             |             |
        //    P     |             |      o      |
        //    i     |             ^             |
        //    t   0 +-------------X-------------+   0.5
        //    c     |             |             |
        //    h     |             |             |
        //          |             |             |
        //      -90 +-------------+-------------+   1.0
        //          0.0          0.5          1.0
        //    X  - the default camera center
        //    ^  - the default up vector
        //    o  - the image center for a pitch of 45 and a heading of 90
        //    Coords on left and top sides are degrees
        //    Coords on right and bottom axes are our X/Y in range [0..1)
        //  Note: Negative field of view can be supplied to flip the image.
        *outX = ((x - 0.5f) * ctx->fixed_hfov + ctx->fixed_yaw)   / 360.0f
            + 0.5f;
        *outY = ((y - 0.5f) * ctx->fixed_vfov - ctx->fixed_pitch) / 180.0f
            + 0.5f;

        normalize_equirectangular(*outX, *outY, outX, outY);
    } else if (ctx->output_layout == LAYOUT_CUBEMAP ||
            ctx->output_layout == LAYOUT_PLANE_POLES_CUBEMAP ||
            ctx->output_layout == LAYOUT_CUBEMAP_32 ||
            ctx->output_layout == LAYOUT_CUBEMAP_180 ||
            ctx->output_layout == LAYOUT_PLANE_CUBEMAP_32 ||
            ctx->output_layout == LAYOUT_PLANE_CUBEMAP ||
            ctx->output_layout == LAYOUT_BARREL ||
            ctx->output_layout == LAYOUT_TB_ONLY ||
            ctx->output_layout == LAYOUT_TB_BARREL_ONLY) {
        float qx, qy, qz;
        float cos_y, cos_p, sin_y, sin_p;
        float tx, ty, tz;
        float yaw, pitch;
        float d;
        y = 1.0f - y;

        const float *vx, *vy, *p;
        int face = 0;
        if (ctx->output_layout == LAYOUT_CUBEMAP) {
            face = (int) (x * 6);
            x = x * 6.0f - face;
        } else if (ctx->output_layout == LAYOUT_CUBEMAP_32) {
            int vface = (int) (y * 2);
            int hface = (int) (x * 3);
            x = x * 3.0f - hface;
            y = y * 2.0f - vface;
            face = hface + (1 - vface) * 3;
        } else if (ctx->output_layout == LAYOUT_CUBEMAP_180) {
            // LAYOUT_CUBEMAP_180: layout for spatial resolution downsampling with 180 degree viewport size
            //
            // - Given a view (yaw,pitch) we can create a customized cube mapping to make the view center at the front cube face.
            // - A 180 degree viewport cut the cube into 2 equal-sized halves: front half and back half.
            // - The front half contains these faces of the cube: front, half of right, half of left, half of top, half of bottom.
            //   The back half contains these faces of the cube: back, half of right, half of left, half of top, half of bottom.
            //   Illutrasion on LAYOUT_CUBEMAP_32 (mono):
            //
            //   +---+---+---+---+---+---+
            //   |   |   |   |   |   5   |
            //   + 1 | 2 + 3 | 4 +-------+     Area 1, 4, 6, 7, 9 are in the front half
            //   |   |   |   |   |   6   |
            //   +---+---+---+---+---+---+     Area 2, 3, 5, 8, 0 are in the back half
            //   |   7   |       |       |
            //   +-------+   9   +   0   +
            //   |   8   |       |       |
            //   +---+---+---+---+---+---+
            //
            // - LAYOUT_CUBEMAP_180 reduces the spatial resolution of the back half to 25% (1/2 height, 1/2 width makes 1/4 size)
            //   and then re-pack the cube map like this:
            //
            //   +---+---+---+---+---+      Front half   Back half (1/4 size)
            //   |       |   |   c   |      ----------   --------------------
            //   +   a   + b +---+----      Area a = 9   Area f = 0
            //   |       |   | f |   |      Area b = 4   Area g = 3
            //   +---+---+---+---+ d +      Area c = 6   Area h = 2
            //   |g|h|-i-|   e   |   |      Area d = 1   Area i1(top) = 5
            //   +---+---+---+---+---+      Area e = 7   Area i2(bottom) = 8
            //
            if (0.0f <= y && y < 1.0f/3 && 0.0f <= x && x < 0.8f) { // Area g, h, i1, i2, e
                if (0.0f <= x && x < 0.1f) { // g
                    face = LEFT;
                    x = x/0.2f;
                    y = y/(1.0f/3);
                }
                else if (0.1f <= x && x < 0.2f) { // h
                    face = RIGHT;
                    x = (x-0.1f)/0.2f + 0.5f;
                    y = y/(1.0f/3);
                }
                else if (0.2f <= x && x < 0.4f) {
                    if (y >= 1.0f/6){ //i1
                        face = TOP;
                        x = (x-0.2f)/0.2f;
                        y  =(y-1.0f/6)/(1.0f/3) + 0.5f;
                    }
                    else { // i2
                        face = BOTTOM;
                        x = (x-0.2f)/0.2f;
                        y = y/(1.0f/3);
                    }
                }
                else if (0.4f <= x && x < 0.8f){ // e
                    face = BOTTOM;
                    x = (x-0.4f)/0.4f;
                    y = y/(2.0f/3) + 0.5f;
                }
            }
            else if (2.0f/3 <= y && y < 1.0f && 0.6f <= x && x < 1.0f) { // Area c
                face = TOP;
                x = (x-0.6f)/0.4f;
                y = (y-2.0f/3)/(2.0f/3);
            }
            else { // Area a, b, f, d
                if (0.0f <= x && x < 0.4f) { // a
                    face = FRONT;
                    x = x/0.4f;
                    y = (y-1.0/3)/(2.0f/3);
                }
                else if (0.4f <= x && x < 0.6f) { // b
                    face = LEFT;
                    x = (x-0.4f)/0.4f + 0.5f;
                    y = (y-1.0f/3)/(2.0f/3);
                }
                else if (0.6f <= x && x < 0.8f) { // f
                    face = BACK;
                    x = (x-0.6f)/0.2f;
                    y = (y-1.0f/3)/(1.0f/3);
                }
                else if (0.8f <= x && x < 1.0f) { // d
                    face = RIGHT;
                    x = (x-0.8f)/0.4f;
                    y = y/(2.0f/3);
                }
            }
        } else if (ctx->output_layout == LAYOUT_PLANE_CUBEMAP_32) {
            int vface = (int) (y * 2);
            int hface = (int) (x * 3);
            x = x * 3.0f - hface;
            y = y * 2.0f - vface;
            face = hface + (1 - vface) * 3;
            face = PLANE_CUBEMAP_32_FACE_MAP[face];
        } else if (ctx->output_layout == LAYOUT_PLANE_POLES_CUBEMAP) {
            face = (int) (x * 4.5f);
            x = x * 4.5f - face;
            if (face == 4) {
                x *= 2.0f;
                y *= 2.0f;
                if (y >= 1.0f) {
                    y -= 1.0f;
                } else {
                    face = 5; // bottom
                }
            }
            face = PLANE_POLES_FACE_MAP[face];
        } else if (ctx->output_layout == LAYOUT_PLANE_CUBEMAP) {
            face = (int) (x * 6);
            x = x * 6.0f - face;
            face = PLANE_CUBEMAP_FACE_MAP[face];
        } else if (ctx->output_layout == LAYOUT_BARREL) {
          if (x <= 0.8f) {
            yaw = (2.5f * x - 1.0f) * ctx->expand_coef * M_PI;
            pitch = (y * 0.5f  - 0.25f) * ctx->expand_coef * M_PI;
            face = -1;
          } else {
            int vFace = (int) (y * 2);
            face = (vFace == 1) ? TOP : BOTTOM;
            x = x * 5.0f - 4.0f;
            y = y * 2.0f - vFace;
          }
        } else if (ctx->output_layout == LAYOUT_TB_ONLY ||
                ctx->output_layout == LAYOUT_TB_BARREL_ONLY) {
          int vFace = (int) (y * 2);
          face = (vFace == 1) ? TOP : BOTTOM;
          y = y * 2.0f - vFace;
        } else {
            av_assert0(0);
        }

        if (ctx->output_layout == LAYOUT_BARREL && face < 0) {
          float sin_yaw = sin(yaw);
          float sin_pitch = sin(pitch);
          float cos_yaw = cos(yaw);
          float cos_pitch = cos(pitch);
          qx = sin_yaw * cos_pitch;
          qy = sin_pitch;
          qz = cos_yaw * cos_pitch;
        } else {
          av_assert1(x >= 0 && x <= 1);
          av_assert1(y >= 0 && y <= 1);
          av_assert1(face >= 0 && face < 6);

          if (ctx->output_layout == LAYOUT_BARREL ||
            ctx->output_layout == LAYOUT_TB_BARREL_ONLY) {
            float radius = (x - 0.5f) * (x - 0.5f) + (y - 0.5f) * (y - 0.5f);
            if (radius > 0.25f * ctx->expand_coef * ctx->expand_coef) {
              *has_mapping = 0;
              return;
            }
          }

          x = (x - 0.5f) * ctx->expand_coef + 0.5f;
          y = (y - 0.5f) * ctx->expand_coef + 0.5f;

          switch (face) {
              case RIGHT:   p = P5; vx = NZ; vy = PY; break;
              case LEFT:    p = P0; vx = PZ; vy = PY; break;
              case TOP:     p = P6; vx = PX; vy = NZ; break;
              case BOTTOM:  p = P0; vx = PX; vy = PZ; break;
              case FRONT:   p = P4; vx = PX; vy = PY; break;
              case BACK:    p = P1; vx = NX; vy = PY; break;
          }
          qx = p [0] + vx [0] * x + vy [0] * y;
          qy = p [1] + vx [1] * x + vy [1] * y;
          qz = p [2] + vx [2] * x + vy [2] * y;
        }

        // rotation
        sin_y = sin(ctx->fixed_yaw*M_PI/180.0f);
        sin_p = sin(ctx->fixed_pitch*M_PI/180.0f);
        cos_y = cos(ctx->fixed_yaw*M_PI/180.0f);
        cos_p = cos(ctx->fixed_pitch*M_PI/180.0f);
        tx = qx * cos_y   - qy * sin_y*sin_p  + qz * sin_y*cos_p;
        ty =                qy * cos_p        + qz * sin_p;
        tz = qx* (-sin_y) - qy * cos_y*sin_p  + qz * cos_y*cos_p;

        d = sqrtf(tx * tx + ty * ty + tz * tz);
        *outX = -atan2f (-tx / d, tz / d) / (M_PI * 2.0f) + 0.5f;
        if (ctx->output_layout == LAYOUT_BARREL) {
          *outX = FFMIN(*outX, 1.0f - input_pixel_w * 0.5f);
          *outX = FFMAX(*outX, input_pixel_w * 0.5f);
        }
        *outY = asinf (-ty / d) / M_PI + 0.5f;
    }

    if (ctx->input_stereo_format == STEREO_FORMAT_TB) {
        if (is_right) {
            *outY = *outY * Y_HALF + Y_HALF;
        } else {
            *outY = *outY * Y_HALF;
        }
    } else if (ctx->input_stereo_format == STEREO_FORMAT_LR) {
        if (is_right) {
            *outX = *outX * X_HALF + X_HALF;
        } else {
            *outX = *outX * X_HALF;
        }
    } else {
        // mono no steps needed.
    }
    av_assert1(*outX >= 0 && *outX <= 1);
    av_assert1(*outY >= 0 && *outY <= 1);
}

static inline int increase_pixel_weight(TransformPixelWeights *ws, uint32_t id) {
    if (ws->n == 0) {
        ws->pairs = av_malloc_array(INITIAL_PAIR_SIZE, sizeof(*ws->pairs));
        if (!ws->pairs) {
            return AVERROR(ENOMEM);
        }
        *ws->pairs = PACK_PAIR(id, 1);
        ++ws->n;
        return 0;
    }

    // Looking for existing id
    for (int i = 0; i < ws->n; ++i) {
        if (UNPACK_ID(ws->pairs[i]) == id) {
            ++ws->pairs[i]; // since weight is packed in the lower bits, it works
            return 0;
        }
    }

    // if n is a power of 2, then we need to grow the array
    // grow array by power of 2, copy elements over
    if ((ws->n >= INITIAL_PAIR_SIZE) && !(ws->n & (ws->n - 1))) {
        uint32_t *new_pairs = av_malloc_array(ws->n * 2, sizeof(*ws->pairs));
        if (!new_pairs) {
            return AVERROR(ENOMEM);
        }
        memcpy(new_pairs, ws->pairs, sizeof(*ws->pairs) * ws->n);
        av_freep(&ws->pairs);
        ws->pairs = new_pairs;
    }

    ws->pairs[ws->n] = PACK_PAIR(id, 1);
    ++ws->n;

    return 0;
}

static inline int generate_map(TransformContext *s,
        AVFilterLink *inlink, AVFilterLink *outlink, AVFrame *in) {
    AVFilterContext *ctx = outlink->src;

    const AVPixFmtDescriptor *desc = av_pix_fmt_desc_get(outlink->format);
    s->planes = av_pix_fmt_count_planes(outlink->format);
    s->out_map_planes = 2;
    s->out_map = av_malloc_array(s->out_map_planes, sizeof(*s->out_map));
    if (!s->out_map) {
        return AVERROR(ENOMEM);
    }

    for (int plane = 0; plane < s->out_map_planes; ++plane) {
        int out_w, out_h, in_w, in_h;
        TransformPlaneMap *p;
        av_log(ctx, AV_LOG_VERBOSE, "processing plane #%d\n",
                plane);
        out_w = outlink->w;
        out_h = outlink->h;
        in_w = inlink->w;
        in_h = inlink->h;

        if (plane == 1) {
            out_w = FF_CEIL_RSHIFT(out_w, desc->log2_chroma_w);
            out_h = FF_CEIL_RSHIFT(out_h, desc->log2_chroma_h);
            in_w = FF_CEIL_RSHIFT(in_w, desc->log2_chroma_w);
            in_h = FF_CEIL_RSHIFT(in_h, desc->log2_chroma_h);
        }
        p = &s->out_map[plane];
        p->w = out_w;
        p->h = out_h;
        p->weights = av_malloc_array(out_w * out_h, sizeof(*p->weights));
        if (!p->weights) {
            return AVERROR(ENOMEM);
        }

        float input_pixel_w = 1.0f / in_w;
        if (s->input_stereo_format == STEREO_FORMAT_LR) {
          input_pixel_w *= 2;
        }

        for (int i = 0; i < out_h; ++i) {
            for (int j = 0; j < out_w; ++j) {
                int id = i * out_w + j;
                float out_x, out_y;
                TransformPixelWeights *ws = &p->weights[id];
                ws->n = 0;
                for (int suby = 0; suby < s->h_subdivisions; ++suby) {
                    for (int subx = 0; subx < s->w_subdivisions; ++subx) {
                        float y = (i + (suby + 0.5f) / s->h_subdivisions) / out_h;
                        float x = (j + (subx + 0.5f) / s->w_subdivisions) / out_w;
                        int in_x, in_y;
                        uint32_t in_id;
                        int result;
                        int has_mapping;
                        transform_pos(
                          s, x, y, &out_x, &out_y, &has_mapping, input_pixel_w);

                        if (!has_mapping) {
                          continue;
                        }

                        in_y = (int) (out_y * in_h);
                        in_x = (int) (out_x * in_w);

                        in_id = in_y * in->linesize[plane] + in_x;
                        result = increase_pixel_weight(ws, in_id);
                        if (result != 0) {
                            return result;
                        }
                    }
                }
            }
        }
    }
    return 0;
}

static int config_output(AVFilterLink *outlink)
{
    AVFilterContext *ctx = outlink->src;
    AVFilterLink *inlink = outlink->src->inputs[0];
    TransformContext *s = ctx->priv;
    double var_values[VARS_NB], res;
    char *expr;
    int ret;

    var_values[VAR_OUT_W] = var_values[VAR_OW] = NAN;
    var_values[VAR_OUT_H] = var_values[VAR_OH] = NAN;

    if (s->input_stereo_format == STEREO_FORMAT_GUESS) {
        int aspect_ratio = inlink->w / inlink->h;
        if (aspect_ratio == 1)
            s->input_stereo_format = STEREO_FORMAT_TB;
        else if (aspect_ratio == 4)
            s->input_stereo_format = STEREO_FORMAT_LR;
        else
            s->input_stereo_format = STEREO_FORMAT_MONO;
    }

    if (s->max_cube_edge_length > 0) {
        if (s->input_stereo_format == STEREO_FORMAT_LR) {
            s->cube_edge_length = inlink->w / 8;
        } else {
            s->cube_edge_length = inlink->w / 4;
        }

        // do not exceed the max length supplied
        if (s->cube_edge_length > s->max_cube_edge_length) {
            s->cube_edge_length = s->max_cube_edge_length;
        }
    }

    // ensure cube edge length is a multiple of 16 by rounding down
    // so that macroblocks do not cross cube edge boundaries
    s->cube_edge_length = s->cube_edge_length - (s->cube_edge_length % 16);

    if (s->cube_edge_length > 0) {
        if (s->output_layout == LAYOUT_CUBEMAP || s->output_layout == LAYOUT_PLANE_CUBEMAP) {
            outlink->w = s->cube_edge_length * 6;
            outlink->h = s->cube_edge_length;

            if (s->input_stereo_format == STEREO_FORMAT_TB || s->input_stereo_format == STEREO_FORMAT_LR)
                outlink->h = outlink->h * 2;
        } else if (s->output_layout == LAYOUT_CUBEMAP_32 || s->output_layout == LAYOUT_PLANE_CUBEMAP_32) {
            outlink->w = s->cube_edge_length * 3;
            outlink->h = s->cube_edge_length * 2;

            if (s->input_stereo_format == STEREO_FORMAT_TB || s->input_stereo_format == STEREO_FORMAT_LR)
                outlink->h = outlink->h * 2;
        } else if (s->output_layout == LAYOUT_CUBEMAP_180) {
            outlink->w = s->cube_edge_length * 2.5;
            outlink->h = s->cube_edge_length * 1.5;

            if (s->input_stereo_format == STEREO_FORMAT_TB || s->input_stereo_format == STEREO_FORMAT_LR)
                outlink->h = outlink->h * 2;
        }
    } else {
        var_values[VAR_OUT_W] = var_values[VAR_OW] = NAN;
        var_values[VAR_OUT_H] = var_values[VAR_OH] = NAN;

        av_expr_parse_and_eval(&res, (expr = s->w_expr),
                var_names, var_values,
                NULL, NULL, NULL, NULL, NULL, 0, ctx);
        s->w = var_values[VAR_OUT_W] = var_values[VAR_OW] = res;
        if ((ret = av_expr_parse_and_eval(&res, (expr = s->h_expr),
                        var_names, var_values,
                        NULL, NULL, NULL, NULL, NULL, 0, ctx)) < 0) {
            av_log(NULL, AV_LOG_ERROR,
                    "Error when evaluating the expression '%s'.\n"
                    "Maybe the expression for out_w:'%s' or for out_h:'%s' is self-referencing.\n",
                    expr, s->w_expr, s->h_expr);
            return ret;
        }
        s->h = var_values[VAR_OUT_H] = var_values[VAR_OH] = res;
        /* evaluate again the width, as it may depend on the output height */
        if ((ret = av_expr_parse_and_eval(&res, (expr = s->w_expr),
                        var_names, var_values,
                        NULL, NULL, NULL, NULL, NULL, 0, ctx)) < 0) {
            av_log(NULL, AV_LOG_ERROR,
                    "Error when evaluating the expression '%s'.\n"
                    "Maybe the expression for out_w:'%s' or for out_h:'%s' is self-referencing.\n",
                    expr, s->w_expr, s->h_expr);
            return ret;
        }
        s->w = res;

        outlink->w = s->w;
        outlink->h = s->h;
    }

    av_log(ctx, AV_LOG_VERBOSE, "out_w:%d out_h:%d\n",
            outlink->w, outlink->h);

    return 0;
}

static av_cold int init_dict(AVFilterContext *ctx, AVDictionary **opts)
{
    TransformContext *s = ctx->priv;

    if (s->size_str && (s->w_expr || s->h_expr)) {
        av_log(ctx, AV_LOG_ERROR,
                "Size and width/height expressions cannot be set at the same time.\n");
        return AVERROR(EINVAL);
    }

    if (s->w_expr && !s->h_expr)
        FFSWAP(char *, s->w_expr, s->size_str);

    av_log(ctx, AV_LOG_VERBOSE, "w:%s h:%s\n",
            s->w_expr, s->h_expr);

    s->opts = *opts;
    *opts = NULL;

    return 0;
}

static av_cold void uninit(AVFilterContext *ctx)
{
    TransformContext *s = ctx->priv;

    for (int plane = 0; plane < s->out_map_planes; ++plane) {
        TransformPlaneMap *p = &s->out_map[plane];
        for (int i = 0; i < p->h * p->w; ++i) {
            av_freep(&p->weights[i].pairs);
            p->weights[i].pairs = NULL;
        }
        av_freep(&p->weights);
        p->weights = NULL;
    }
    av_freep(&s->out_map);
    s->out_map = NULL;

    av_dict_free(&s->opts);
    s->opts = NULL;
}

static void filter_slice_boundcheck(
        const int tile_i,
        const int tile_j,
        const int linesize,
        const int subs,
        const TransformPlaneMap *p,
        const uint8_t* in_data, uint8_t* out_data
        )
{
    for (int i = 0; i < 16; ++i) {
        if (tile_i + i >= p->h) {
            break;
        }

        int out_line = linesize * (tile_i + i);
        int map_line = p->w * (tile_i + i);
        for (int j = 0; j < 16; ++j) {
            if (tile_j + j >= p->w) {
                break;
            }

            int out_sample = out_line + tile_j + j;
            int id = map_line + tile_j + j;
            TransformPixelWeights *ws = &p->weights[id];
            if (ws->n == 1) {
                out_data[out_sample] = in_data[UNPACK_ID(ws->pairs[0])];
            } else {
                int color_sum = 0;
                for (int k = 0; k < ws->n; ++k) {
                    color_sum += ((int) in_data[UNPACK_ID(ws->pairs[k])]) *
                        UNPACK_COUNT(ws->pairs[k]);
                }
                // Round to nearest
                out_data[out_sample] = (uint8_t) ((color_sum + (subs >> 1)) / subs);
            }
        }
    }
}

static int filter_slice(AVFilterContext *ctx, void *arg, int jobnr, int nb_jobs)
{
    const ThreadData *td = arg;
    const TransformPlaneMap *p = td->p;
    const int linesize = td->linesize;
    const int subs = td->subs;
    const int num_tiles = td->num_tiles;
    const int num_tiles_col = td->num_tiles_col;
    const uint8_t *in_data = td->in_data;
    uint8_t *out_data = td->out_data;

    const int tile_start = (num_tiles * jobnr) / nb_jobs;
    const int tile_end = (num_tiles * (jobnr+1)) / nb_jobs;

    for (int tile = tile_start ; tile < tile_end ; ++tile) {
        int tile_i = (tile / num_tiles_col) * 16;
        int tile_j = (tile % num_tiles_col) * 16;

#ifdef SOFTWARE_PREFETCH_OPT
        TransformPixelWeights *ws_prefetch;
        const uint8_t prefetch_lookahead = 8;
        int id_prefetch = p->w * tile_i + tile_j;

        // The loop below prefetches all the weights from array "p" and
        // associated pairs array. The prefetch is only done for the initial
        // lookahead for the first iteration of tile processing loop below so
        // that they are ready to be consumed in the inner loop. In the tile
        // processing loop, we are prefetching addresses that are after the
        // lookahead (i.e in the same iteration and also the next
        // ietartion of the loop).
        for(int k = 0; k < prefetch_lookahead; ++k){
           ws_prefetch = &p->weights[id_prefetch+k];
           __builtin_prefetch (ws_prefetch, 0, 0);
           __builtin_prefetch (ws_prefetch->pairs, 0, 0);
        }
        // Prefetch the cacheline for out_data for writes
        int out_sample_prefetch = linesize * (tile_i + 2) + tile_j;
        __builtin_prefetch (&out_data[out_sample_prefetch], 1, 0);
#endif

        if ((tile_i + 15) >= p->h || (tile_j + 15) >= p->w) {
            filter_slice_boundcheck(tile_i, tile_j, linesize, subs, p, in_data, out_data);
            continue;
        }

        for (int i = 0; i < 16; ++i) {
            int out_line = linesize * (tile_i + i);
            int map_line = p->w * (tile_i + i);

#ifdef SOFTWARE_PREFETCH_OPT
            // Prefetch the cacheline for out_data for writes
            __builtin_prefetch (&out_data[out_line+tile_j], 1, 0);
#endif

            for (int j = 0; j < 16; ++j) {
                int out_sample = out_line + tile_j + j;
                int id = map_line + tile_j + j;
                TransformPixelWeights *ws = &p->weights[id];

#ifdef SOFTWARE_PREFETCH_OPT
                // In this inner loop, we prefech the weight from array "p" after the
                // prefetch_lookahead iteration. We also prefetch the weight pairs
                // along with weight address as we found that we were getting
                // datacache (L1 and LLC) and DTLB misses for both the address
                // and the pair.
                if (j <  prefetch_lookahead) {
                   ws_prefetch = &p->weights[id+prefetch_lookahead];
                   __builtin_prefetch (ws_prefetch->pairs, 0, 0);
                   __builtin_prefetch (ws_prefetch+prefetch_lookahead, 0, 0);
                }
                else if (i < 15) {
                   // Here we are prefetching the address for the next iteration of outer loop
                   // so that we have the data avaialble in the next loop when it starts.
                   id_prefetch = p->w + id - prefetch_lookahead;
                   ws_prefetch = &p->weights[id_prefetch];
                   __builtin_prefetch (ws_prefetch->pairs, 0, 0);
                   __builtin_prefetch (ws_prefetch+prefetch_lookahead, 0, 0);
                }
                // Prefetch the cacheline for out_data for writes
                __builtin_prefetch (&out_data[out_sample], 1, 0);
#endif

                if (ws->n == 1) {
                    out_data[out_sample] = in_data[UNPACK_ID(ws->pairs[0])];
                } else {
                    int color_sum = 0;
                    for (int k = 0; k < ws->n; ++k) {
                        color_sum += ((int) in_data[UNPACK_ID(ws->pairs[k])]) *
                            UNPACK_COUNT(ws->pairs[k]);
                    }
                    // Round to nearest
                    out_data[out_sample] = (uint8_t) ((color_sum + (subs >> 1)) / subs);
                }
            }
        }
    }

    return 0;
}

static int filter_frame(AVFilterLink *inlink, AVFrame *in)
{
    AVFilterContext *ctx = inlink->dst;
    TransformContext *s = ctx->priv;
    AVFilterLink *outlink = ctx->outputs[0];
    AVFrame *out;
    int subs;
    av_log(ctx, AV_LOG_VERBOSE, "Frame\n");

    // map not yet set
    if (s->out_map_planes != 2) {
        int result = generate_map(s, inlink, outlink, in);
        if (result != 0) {
            av_frame_free(&in);
            return result;
        }
    }

    out = ff_get_video_buffer(outlink, outlink->w, outlink->h);
    av_log(ctx, AV_LOG_VERBOSE, "Got Frame %dx%d\n", outlink->w, outlink->h);

    if (!out) {
        av_frame_free(&in);
        return AVERROR(ENOMEM);
    }
    av_frame_copy_props(out, in);
    av_log(ctx, AV_LOG_VERBOSE, "Copied props \n");
    subs = s->w_subdivisions * s->h_subdivisions;

    for (int plane = 0; plane < s->planes; ++plane) {
        uint8_t *in_data, *out_data;
        int out_map_plane;
        TransformPlaneMap *p;

        in_data = in->data[plane];
        av_assert1(in_data);
        out_map_plane = (plane == 1 || plane == 2) ? 1 : 0;
        p = &s->out_map[out_map_plane];
        out_data = out->data[plane];

        int num_tiles_row = 1 + ((p->h - 1) / 16); // ceiling operation
        int num_tiles_col = 1 + ((p->w - 1) / 16); // ceiling operation
        int num_tiles = num_tiles_row * num_tiles_col;

        ThreadData td;
        td.p = p;
        td.subs = subs;
        td.linesize = out->linesize[plane];
        td.num_tiles = num_tiles;
        td.num_tiles_col = num_tiles_col;
        td.in_data = in_data;
        td.out_data = out_data;
        ctx->internal->execute(ctx, filter_slice, &td, NULL, FFMIN(num_tiles, ctx->graph->nb_threads));
    }

    av_log(ctx, AV_LOG_VERBOSE, "Done with byte copy \n");

    av_frame_free(&in);
    av_log(ctx, AV_LOG_VERBOSE, "Done freeing in \n");
    return ff_filter_frame(outlink, out);
}

#define OFFSET(x) offsetof(TransformContext, x)
#define FLAGS AV_OPT_FLAG_VIDEO_PARAM|AV_OPT_FLAG_FILTERING_PARAM

static const AVOption transform_options[] = {
    { "w",             "Output video width",          OFFSET(w_expr),    AV_OPT_TYPE_STRING, {.str = "0"}, CHAR_MIN, CHAR_MAX, FLAGS },
    { "width",         "Output video width",          OFFSET(w_expr),    AV_OPT_TYPE_STRING, {.str = "0"}, CHAR_MIN, CHAR_MAX, FLAGS },
    { "h",             "Output video height",         OFFSET(h_expr),    AV_OPT_TYPE_STRING, {.str = "0"}, CHAR_MIN, CHAR_MAX, FLAGS },
    { "height",        "Output video height",         OFFSET(h_expr),    AV_OPT_TYPE_STRING, {.str = "0"}, CHAR_MIN, CHAR_MAX, FLAGS },
    { "size",          "set video size",              OFFSET(size_str), AV_OPT_TYPE_STRING, {.str = NULL}, 0, FLAGS },
    { "s",             "set video size",              OFFSET(size_str), AV_OPT_TYPE_STRING, {.str = NULL}, 0, FLAGS },
    { "cube_edge_length", "Length of a cube edge (for cubic transform, overrides w and h, default 0 for off)",         OFFSET(cube_edge_length),    AV_OPT_TYPE_INT,  {.i64 = 0}, 0, 16384,  .flags = FLAGS },
    { "max_cube_edge_length", "Max length of a cube edge (for cubic transform, overrides w, h, and cube_edge_length, default 0 for off)",   OFFSET(max_cube_edge_length),    AV_OPT_TYPE_INT,  {.i64 = 0}, 0, 16384,  .flags = FLAGS },
    { "input_stereo_format", "Input video stereo format",         OFFSET(input_stereo_format),    AV_OPT_TYPE_INT,  {.i64 = STEREO_FORMAT_GUESS }, 0, STEREO_FORMAT_N - 1,  .flags = FLAGS, "stereo_format" },
    { "TB",      NULL, 0, AV_OPT_TYPE_CONST, {.i64 = STEREO_FORMAT_TB },      0, 0, FLAGS, "stereo_format" },
    { "LR",      NULL, 0, AV_OPT_TYPE_CONST, {.i64 = STEREO_FORMAT_LR },      0, 0, FLAGS, "stereo_format" },
    { "MONO",    NULL, 0, AV_OPT_TYPE_CONST, {.i64 = STEREO_FORMAT_MONO },    0, 0, FLAGS, "stereo_format" },
    { "GUESS",   NULL, 0, AV_OPT_TYPE_CONST, {.i64 = STEREO_FORMAT_GUESS },   0, 0, FLAGS, "stereo_format" },
    { "tb",      NULL, 0, AV_OPT_TYPE_CONST, {.i64 = STEREO_FORMAT_TB },      0, 0, FLAGS, "stereo_format" },
    { "lr",      NULL, 0, AV_OPT_TYPE_CONST, {.i64 = STEREO_FORMAT_LR },      0, 0, FLAGS, "stereo_format" },
    { "mono",    NULL, 0, AV_OPT_TYPE_CONST, {.i64 = STEREO_FORMAT_MONO },    0, 0, FLAGS, "stereo_format" },
    { "guess",   NULL, 0, AV_OPT_TYPE_CONST, {.i64 = STEREO_FORMAT_GUESS },   0, 0, FLAGS, "stereo_format" },
    { "output_layout", "Output video layout format",         OFFSET(output_layout),    AV_OPT_TYPE_INT,  {.i64 = LAYOUT_CUBEMAP_32 }, 0, LAYOUT_N - 1,  .flags = FLAGS, "layout" },
    { "CUBEMAP",             NULL, 0, AV_OPT_TYPE_CONST, {.i64 = LAYOUT_CUBEMAP },             0, 0, FLAGS, "layout" },
    { "CUBEMAP_32",          NULL, 0, AV_OPT_TYPE_CONST, {.i64 = LAYOUT_CUBEMAP_32 },          0, 0, FLAGS, "layout" },
    { "CUBEMAP_180",         NULL, 0, AV_OPT_TYPE_CONST, {.i64 = LAYOUT_CUBEMAP_180 },         0, 0, FLAGS, "layout" },
    { "PLANE_POLES",         NULL, 0, AV_OPT_TYPE_CONST, {.i64 = LAYOUT_PLANE_POLES },         0, 0, FLAGS, "layout" },
    { "PLANE_POLES_6",       NULL, 0, AV_OPT_TYPE_CONST, {.i64 = LAYOUT_PLANE_POLES_6 },       0, 0, FLAGS, "layout" },
    { "PLANE_POLES_CUBEMAP", NULL, 0, AV_OPT_TYPE_CONST, {.i64 = LAYOUT_PLANE_POLES_CUBEMAP }, 0, 0, FLAGS, "layout" },
    { "PLANE_CUBEMAP",       NULL, 0, AV_OPT_TYPE_CONST, {.i64 = LAYOUT_PLANE_CUBEMAP },       0, 0, FLAGS, "layout" },
    { "PLANE_CUBEMAP_32",    NULL, 0, AV_OPT_TYPE_CONST, {.i64 = LAYOUT_PLANE_CUBEMAP_32 },    0, 0, FLAGS, "layout" },
    { "FLAT_FIXED",          NULL, 0, AV_OPT_TYPE_CONST, {.i64 = LAYOUT_FLAT_FIXED },          0, 0, FLAGS, "layout" },
    { "BARREL",              NULL, 0, AV_OPT_TYPE_CONST, {.i64 = LAYOUT_BARREL },              0, 0, FLAGS, "layout" },
    { "TB_ONLY",             NULL, 0, AV_OPT_TYPE_CONST, {.i64 = LAYOUT_TB_ONLY },             0, 0, FLAGS, "layout" },
    { "TB_BARREL_ONLY",      NULL, 0, AV_OPT_TYPE_CONST, {.i64 = LAYOUT_TB_BARREL_ONLY },      0, 0, FLAGS, "layout" },
    { "cubemap",             NULL, 0, AV_OPT_TYPE_CONST, {.i64 = LAYOUT_CUBEMAP },             0, 0, FLAGS, "layout" },
    { "cubemap_32",          NULL, 0, AV_OPT_TYPE_CONST, {.i64 = LAYOUT_CUBEMAP_32 },          0, 0, FLAGS, "layout" },
    { "cubemap_180",         NULL, 0, AV_OPT_TYPE_CONST, {.i64 = LAYOUT_CUBEMAP_180 },         0, 0, FLAGS, "layout" },
    { "plane_poles",         NULL, 0, AV_OPT_TYPE_CONST, {.i64 = LAYOUT_PLANE_POLES },         0, 0, FLAGS, "layout" },
    { "plane_poles_6",       NULL, 0, AV_OPT_TYPE_CONST, {.i64 = LAYOUT_PLANE_POLES_6 },       0, 0, FLAGS, "layout" },
    { "plane_poles_cubemap", NULL, 0, AV_OPT_TYPE_CONST, {.i64 = LAYOUT_PLANE_POLES_CUBEMAP }, 0, 0, FLAGS, "layout" },
    { "plane_cubemap",       NULL, 0, AV_OPT_TYPE_CONST, {.i64 = LAYOUT_PLANE_CUBEMAP },       0, 0, FLAGS, "layout" },
    { "plane_cubemap_32",    NULL, 0, AV_OPT_TYPE_CONST, {.i64 = LAYOUT_PLANE_CUBEMAP_32 },    0, 0, FLAGS, "layout" },
    { "flat_fixed",          NULL, 0, AV_OPT_TYPE_CONST, {.i64 = LAYOUT_FLAT_FIXED },          0, 0, FLAGS, "layout" },
    { "barrel",              NULL, 0, AV_OPT_TYPE_CONST, {.i64 = LAYOUT_BARREL },              0, 0, FLAGS, "layout" },
    { "tb_only",             NULL, 0, AV_OPT_TYPE_CONST, {.i64 = LAYOUT_TB_ONLY },             0, 0, FLAGS, "layout" },
    { "tb_barrel_only",      NULL, 0, AV_OPT_TYPE_CONST, {.i64 = LAYOUT_TB_BARREL_ONLY },      0, 0, FLAGS, "layout" },
    { "vflip", "Output video 2nd eye vertical flip (true, false)",         OFFSET(vflip),    AV_OPT_TYPE_INT, {.i64 = 0 }, 0, 1,     .flags = FLAGS, "vflip" },
    { "false",  NULL, 0, AV_OPT_TYPE_CONST, {.i64 = 0 }, 0, 0, FLAGS, "vflip" },
    { "true",   NULL, 0, AV_OPT_TYPE_CONST, {.i64 = 1 }, 0, 0, FLAGS, "vflip" },
    { "main_plane_ratio", "Output video main plain ratio for PLANE_POLES format (0.88888)",         OFFSET(main_plane_ratio),    AV_OPT_TYPE_FLOAT,  {.dbl=MAIN_PLANE_WIDTH}, 0, 1,  .flags = FLAGS },
    { "expand_coef", "Expansion coeffiecient for each face in cubemap (default 1.01)",         OFFSET(expand_coef),    AV_OPT_TYPE_FLOAT,  {.dbl=1.01f}, 0, 10,  .flags = FLAGS },
    { "w_subdivisions", "Number of horizontal per-pixel subdivisions for better downsampling (default 8)",         OFFSET(w_subdivisions),    AV_OPT_TYPE_INT,  {.i64 = 8}, 1, 8,  .flags = FLAGS },
    { "h_subdivisions", "Number of vertical per-pixel subdivisions for better downsampling (default 8)",         OFFSET(h_subdivisions),    AV_OPT_TYPE_INT,  {.i64 = 8}, 1, 8,  .flags = FLAGS },
    { "yaw", "View orientation for flat_fixed projection, degrees",   OFFSET(fixed_yaw),          AV_OPT_TYPE_FLOAT,   {.dbl =   0.0}, -360, 360,  .flags = FLAGS },
    { "pitch", "View orientation for flat_fixed projection, degrees", OFFSET(fixed_pitch),        AV_OPT_TYPE_FLOAT,   {.dbl =   0.0},  -90,  90,  .flags = FLAGS },
    { "hfov", "Horizontal field of view for flat_fixed projection, degrees (default 120)",  OFFSET(fixed_hfov), AV_OPT_TYPE_FLOAT,   {.dbl = 120.0}, -360, 360,  .flags = FLAGS },
    { "vfov", "Vertical field of view for flat_fixed projection, degrees (default 110)",     OFFSET(fixed_vfov), AV_OPT_TYPE_FLOAT,   {.dbl = 110.0}, -180, 180,  .flags = FLAGS },
    { NULL }
};

static const AVClass transform_class = {
    .class_name       = "transform_v1",
    .item_name        = av_default_item_name,
    .option           = transform_options,
    .version          = LIBAVUTIL_VERSION_INT,
    .category         = AV_CLASS_CATEGORY_FILTER,
};

static const AVFilterPad avfilter_vf_transform_inputs[] = {
    {
        .name         = "default",
        .type         = AVMEDIA_TYPE_VIDEO,
        .filter_frame = filter_frame,
    },
    { NULL }
};

static const AVFilterPad avfilter_vf_transform_outputs[] = {
    {
        .name = "default",
        .type = AVMEDIA_TYPE_VIDEO,
        .config_props = config_output,
    },
    { NULL }
};

AVFilter ff_vf_transform_v1 = {
    .name           = "transform_v1",
    .description    = NULL_IF_CONFIG_SMALL("Transforms equirectangular input video to the other format."),
    .init_dict      = init_dict,
    .uninit         = uninit,
    .priv_size      = sizeof(TransformContext),
    .priv_class     = &transform_class,
    .query_formats  = query_formats,
    .inputs         = avfilter_vf_transform_inputs,
    .outputs        = avfilter_vf_transform_outputs,
};
